package host.discoveryProtocol;

import homeMadeExceptions.NotYetImplementedException;
import host.DiscoveryNetworkSocket;
import host.routingProtocol.PubSubRouting;

import java.util.HashMap;
import java.util.Vector;

import datatypes.BluetoothDeviceAddress;
import datatypes.Host;
import datatypes.Message;

public class PubSubDiscovery extends DiscoveryNetworkSocket {

	
	private static final String UPDATE_SERVICE = "U_O_S";
	private static final String UPDATE_FOR_SUBSCRIBER = "u_f_S";
	private static final String PUBLISH_OWN_NEW = "P_O_N";
	private static final String HOST_NOT_LOGER_REACHABLE = "H_N_R";
	
	//the host this socket belongs to
	private Host host = null;
	private boolean discoveryMechanismIsOn = false;
	
	private ServiceTable serviceTable = null;
	
	private enum requestState {
		OPEN, CLOSED, FAILED;
	}
	
	//sendedMessages
	private HashMap<Integer, requestState> messageIDtoStateMap = new HashMap<Integer, requestState>();
	//receivedMessages
	private HashMap<Integer, requestState> messageIDtoAnswerMap = new HashMap<Integer, requestState>();
	
	
	private PubSubDiscovery(Host host) {
		super(host);
		this.host = host;
	}
	
	public static PubSubDiscovery getSocket(Host host){
		return new PubSubDiscovery(host);
	}

	@Override
	public void enableDiscoveryMechanism() {
		this.serviceTable = new ServiceTable(this.host.getBd_Addr(), this.host.getServices());
		super.enableDiscoveryMechanism();
		this.discoveryMechanismIsOn = true;
	}

	@Override
	public void disableDiscoveryMechanism() {
		super.disableDiscoveryMechanism();
		this.discoveryMechanismIsOn = false;
		if (serviceTable != null){
			this.serviceTable.clearServiceTable();			
		}
		this.invokeSendHostNoLogerReachable(null);
	}

	/**
	 * asynchonous --> void (normaly returns a Vector with the Services of the Remote host as String)
	 */
	public void askHostForServices(BluetoothDeviceAddress target) {
		this.processResultAskForService(target, this.serviceTable.getServicesForDevice(target));
	}
	
	/**
	 * asynchonous --> void (normaly returns a Vector with the Provider of the Remote host as BluetoothDeviceAddress)
	 */
	public void findSpecificService(String serviceDescription) {
		Vector<BluetoothDeviceAddress> result = this.serviceTable.getProviderForService(serviceDescription);
		for (BluetoothDeviceAddress singleResult: result){
			this.processResultFindService(singleResult, serviceDescription);	
		}
	}

	/**
	 * asynchonous --> void (normally returns a Vector with the available Services as Strings)
	 */
	public void getAllAvailableServices() {
		if (serviceTable != null){
			this.processResultGetAllServices(null , this.serviceTable.getAllAvailableServices());			
		}
	}

	public void publishLocalServices(String newService) {
		Vector<String> tmp = new Vector<String>();
		tmp.add(newService);
		this.serviceTable.addCombination(this.host.getBd_Addr(), tmp);
		this.host.getServices().add(newService);
		//prepare for NetworkSend
		String messageString = PUBLISH_OWN_NEW;
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(messageString);
		messageContent.add(host.getBd_Addr());
		messageContent.add(this.serviceTable.getServicesForDevice(host.getBd_Addr()));
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, null);		
	}

	public void updateLocalService (String oldService, String newService){
		this.serviceTable.updateService(host.getBd_Addr(), oldService, newService);
		this.host.getServices().remove(oldService);
		this.host.getServices().add(newService);
		//prepare for NetworkSend
		String messageString = UPDATE_SERVICE;
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(messageString);
		messageContent.add(host.getBd_Addr());
		messageContent.add(oldService);
		messageContent.add(newService);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, null);	
	}
	
	public void deleteLocalService (String service){
		this.host.getServices().remove(service);
		this.serviceTable.updateService(host.getBd_Addr(), service, null);
		//prepare for NetworkSend
		String messageString = UPDATE_SERVICE;
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(messageString);
		messageContent.add(host.getBd_Addr());
		messageContent.add(service);
		messageContent.add(null);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, null);
	}
	
	private void invokeSendHostNoLogerReachable(BluetoothDeviceAddress bd_addr) {
		//prepare for NetworkSend
		String messageString = HOST_NOT_LOGER_REACHABLE;
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(messageString);
		messageContent.add(bd_addr);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, bd_addr);	
	}

	/**
	 * second parameter is the channel, not the target
	 * @param msg
	 * @param target
	 */
	private void invokeForwardMessage(Message msg, BluetoothDeviceAddress target) {		
		this.messageIDtoAnswerMap.put(msg.getMsgID_asInteger(), requestState.CLOSED);
		this.invokeSendMessage(msg, target);
	}
	
	/**
	 * second Parameter is the channel , not the target... the target is addressed as Group by the channel
	 */
	@Override
	protected void invokeSendMessage(Message msg, BluetoothDeviceAddress target) {
		this.messageIDtoStateMap.put(msg.getMsgID_asInteger(), requestState.OPEN);
		super.invokeSendMessage(msg, target);
	}

	@Override
	public void notificationAPI() {
		//nothing to do within this protocol
	}

	@Override
	public void onConnectionEstablished(BluetoothDeviceAddress remoteHost) {
		//nothing to do? waiting for the flooding =)
	}

	@Override
	public void onConnectionLost(BluetoothDeviceAddress remoteHost) {
		//if theres no connection available, delete service List
		if (((PubSubRouting)host.getRoutingSocket()).getCurrentConnections().size() == 0){
			this.serviceTable.clearServiceTable();			
		} else {
			this.getXSideHostNotLongerReachable(remoteHost);
		}
	}

	@Override
	public void onMessageACK(Message msg, BluetoothDeviceAddress target) {
		if (this.messageIDtoStateMap.get(msg.getMsgID_asInteger())!= null){
			this.messageIDtoStateMap.remove(msg.getMsgID_asInteger());
			this.messageIDtoStateMap.put(msg.getMsgID_asInteger(), requestState.CLOSED);
		}
		if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger())!= null){
			this.messageIDtoAnswerMap.remove(msg.getMsgID_asInteger());
			this.messageIDtoAnswerMap.put(msg.getMsgID_asInteger(), requestState.CLOSED);
		}
	}

	@Override
	public void onMessageLost(Message msg, BluetoothDeviceAddress target) throws NotYetImplementedException {
		if (this.messageIDtoStateMap.get(msg.getMsgID_asInteger())!= null){
			this.messageIDtoStateMap.remove(msg.getMsgID_asInteger());
			this.messageIDtoStateMap.put(msg.getMsgID_asInteger(), requestState.FAILED);
		}
		if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger())!= null){
			this.messageIDtoAnswerMap.remove(msg.getMsgID_asInteger());
			this.messageIDtoAnswerMap.put(msg.getMsgID_asInteger(), requestState.FAILED);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public void receiveMessage(Message msg, BluetoothDeviceAddress sender) {
		//Handle incoming Message
		if (msg.isDiscoveryMessage()){
			Vector<Object> content = (Vector<Object>) msg.getContent();
			String messageString  = (String)content.get(0);
			if (messageString.equals(UPDATE_FOR_SUBSCRIBER)){
				//only answer if not done before
				if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger()) == null ||
						this.messageIDtoStateMap.get(msg.getMsgID_asInteger()) == requestState.FAILED){
					if (this.serviceTable.updateCombination((BluetoothDeviceAddress)content.get(1), (Vector<String>)content.get(2))){
						//forward to other neighbours
						this.invokeForwardMessage(msg, (BluetoothDeviceAddress)content.get(1));
					}
				}
			} else if (messageString.equals(PUBLISH_OWN_NEW)){
				//same as before, only without reply
				if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger()) == null ||
						this.messageIDtoStateMap.get(msg.getMsgID_asInteger()) == requestState.FAILED){
					if (this.serviceTable.updateCombination((BluetoothDeviceAddress) content.get(1), (Vector<String>)content.get(2))){
						//forward to other neighbours
						this.invokeForwardMessage(msg, null);
					}
				}
			} else if (messageString.equals(UPDATE_SERVICE)){
				//same as before, only without reply
				if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger()) == null ||
						this.messageIDtoStateMap.get(msg.getMsgID_asInteger()) == requestState.FAILED){
					if (this.serviceTable.updateService((BluetoothDeviceAddress)content.get(1), (String) content.get(2), (String) content.get(3))){
						//forward to other neighbours 
						this.invokeForwardMessage(msg, (BluetoothDeviceAddress) content.get(1));
					}
				}
			} else if (messageString.equals(HOST_NOT_LOGER_REACHABLE)){
				if (this.messageIDtoAnswerMap.get(msg.getMsgID_asInteger()) == null ||
						this.messageIDtoStateMap.get(msg.getMsgID_asInteger()) == requestState.FAILED){
					if (this.serviceTable.deleteServicesForProvider((BluetoothDeviceAddress) content.get(1))){
						this.invokeForwardMessage(msg, (BluetoothDeviceAddress) content.get(1));
					};
				}
			}
			}
	}

	

	
	
	
	
	

	private class ServiceTable {
		//service --> providerlist
		private HashMap <String, Vector<BluetoothDeviceAddress>> serviceTable = new HashMap <String, Vector<BluetoothDeviceAddress>>();
		//provider --> servicelist
		private HashMap <String, Vector<String>> providerTable = new HashMap <String, Vector<String>>();
		
		private BluetoothDeviceAddress bd_addr = null;
		private Vector<String> ownServices = null;
		
		public ServiceTable (BluetoothDeviceAddress ownAddress, Vector<String> ownServices){
			this.addCombination(ownAddress, ownServices);
		}
		
		void addCombination(BluetoothDeviceAddress provider , Vector<String> services){
			if (provider != null && services != null ){
				if (bd_addr == null){
					this.bd_addr = provider;
				}
				if (provider.isEqual(bd_addr)){
					
					for (String serv : services){
						if (ownServices == null){
							ownServices = new Vector<String>();
						}
						if (!ownServices.contains(serv)){
							ownServices.add(serv);
						}	
					}
				}
				if (providerTable.get(provider.getAddress()) == null){
					//Provider is unknown --> added with all Services 
					providerTable.put(provider.getAddress(), services);
					//but services should be checked to
					for (String service: services){
						if (serviceTable.get(service) == null){
							Vector<BluetoothDeviceAddress> tmptmp = new Vector<BluetoothDeviceAddress>();
							tmptmp.add(provider);
							serviceTable.put(service, tmptmp);
						} else {
							boolean provIsKnown = false;
							for (BluetoothDeviceAddress provFromList : serviceTable.get(service)){
								if (provFromList.getAddress().equals(provider.getAddress())){
									provIsKnown = true;
								}
							}
							if (!provIsKnown){
								serviceTable.get(service).add(provider);
							}
						}
					}
				} else {

					//Provider is known lets take a look onto the stored services and compare it for actualisation
					Vector<String> servicesOfProvider = providerTable.get(provider.getAddress());
					for (String service: services){
						//service is unknown meens that no provider is owner before
						if (serviceTable.get(service) == null){
							//simple store the provider for the service
							Vector<BluetoothDeviceAddress> tmptmp = new Vector<BluetoothDeviceAddress>();
							tmptmp.add(provider);
							serviceTable.put(service, tmptmp);
							//and the service is new, so the provider needs to become an actualisation
							servicesOfProvider.add(service);
						} else {
							//the service is allready known before, but did i know that is it provided by these device?
							boolean provIsKnown = false;
							for (BluetoothDeviceAddress provFromList : serviceTable.get(service)){
								if (provFromList.getAddress().equals(provider.getAddress())){
									provIsKnown = true;
								}
							}
							//oh yeah i havn't known before, that this device provides the service
							if (!provIsKnown){
								serviceTable.get(service).add(provider);
								//if i don't know that the service is provided by the device
								//i also don't know that the device provides the service
								providerTable.get(provider.getAddress()).add(service);
							} 
							//oh shit everything unnessesary because it was already known that these device provides 
							//this service and vice versa i knows that the service is provided by the device
							else {
								//so here is nothing no no nothing to do
							}
							
						}
					}
				}	
			}
		}

		
		public boolean updateCombination(BluetoothDeviceAddress provider , Vector<String> services){
			boolean thereWasSomethingNew = false;
			if (provider != null && services != null ){
				if (provider.isEqual(bd_addr)){
					for (String serv : services){
						if (ownServices == null){
							ownServices = new Vector<String>();
						}
						if (!ownServices.contains(serv)){
							ownServices.add(serv);
							thereWasSomethingNew = true;
						}	
					}
				}
				if (providerTable.get(provider.getAddress()) == null){
					//Provider is unknown --> added with all Services 
					providerTable.put(provider.getAddress(), services);
					thereWasSomethingNew = true;
					//but services should be checked to
					for (String service: services){
						if (serviceTable.get(service) == null){
							Vector<BluetoothDeviceAddress> tmptmp = new Vector<BluetoothDeviceAddress>();
							tmptmp.add(provider);
							serviceTable.put(service, tmptmp);
						} else {
							boolean provIsKnown = false;
							for (BluetoothDeviceAddress provFromList : serviceTable.get(service)){
								if (provFromList.getAddress().equals(provider.getAddress())){
									provIsKnown = true;
								}
							}
							if (!provIsKnown){
								serviceTable.get(service).add(provider);
								thereWasSomethingNew = true;
							}
						}
					}
				} else {

					//Provider is known lets take a look onto the stored services and compare it for actualisation
					Vector<String> servicesOfProvider = providerTable.get(provider.getAddress());
					for (int i = 0 ; i < services.size(); i++) {
						String service = services.get(i);
						//service is unknown meens that no provider is owner before
						if (serviceTable.get(service) == null){
							//simple store the provider for the service
							Vector<BluetoothDeviceAddress> tmptmp = new Vector<BluetoothDeviceAddress>();
							tmptmp.add(provider);
							serviceTable.put(service, tmptmp);
							//and the service is new, so the provider needs to become an actualisation
							servicesOfProvider.add(service);
							thereWasSomethingNew = true;
						} else {
							//the service is allready known before, but did i know that is it provided by these device?
							boolean provIsKnown = false;
							for (BluetoothDeviceAddress provFromList : serviceTable.get(service)){
								if (provFromList.getAddress().equals(provider.getAddress())){
									provIsKnown = true;
								}
							}
							//oh yeah i havn't known before, that this device provides the service
							if (!provIsKnown){
								serviceTable.get(service).add(provider);
								//if i don't know that the service is provided by the device
								//i also don't know that the device provides the service
								if (providerTable.get(provider.getAddress()) == null){
									Vector<String> tmptmp = new Vector<String>();
									tmptmp.add(service);
									providerTable.put(provider.getAddress(), tmptmp);	
								} else {
									providerTable.get(provider.getAddress()).add(service);	
								}
								thereWasSomethingNew = true;
							} 
							//oh shit everything unnessesary because it was already known that these device provides 
							//this service and vice versa i knows that the service is provided by the device
							else {
								//so here is nothing no no nothing to do
							}
							
						}
					}
				}	
			}
			return thereWasSomethingNew;
		}

		public void deleteService(BluetoothDeviceAddress provider, String service){
			if (provider != null && service != null && 
					this.providerTable.get(provider.getAddress()) != null && this.serviceTable.get(service) != null){
				if (provider.isEqual(bd_addr)){
					ownServices.remove(service);
				}
				if (this.providerTable.get(provider.getAddress()).size() > 1){
					this.providerTable.get(provider.getAddress()).remove(service);	
				} else {
					this.providerTable.remove(provider);
				}
				if (this.serviceTable.get(service).size() > 1){
					this.serviceTable.get(service).remove(provider);
				} else {
					this.serviceTable.remove(service);
				}		
			}
		}

		public boolean deleteServicesForProvider(BluetoothDeviceAddress provider) {
			if (provider != null && this.providerTable.get(provider.getAddress()) != null){
				if (provider.isEqual(bd_addr)){
					ownServices.clear();
				}
				this.providerTable.get(provider.getAddress()).clear();	
				return true;
			}
			return false;
		}

		public boolean updateService(BluetoothDeviceAddress provider, String old, String newS){
			boolean somethingChanged = false;
			//is a delete
			if (newS == null && provider != null && old != null && providerTable.get(provider.getAddress()) != null){
				if (provider.isEqual(bd_addr)){
					if (ownServices == null){
						ownServices = new Vector<String>();
					}
					ownServices.remove(old);
					somethingChanged = true;
				}
				if (providerTable.get(provider.getAddress()).contains(old)){
					//first remove then add new only for this provider
					providerTable.get(provider.getAddress()).remove(old);
					somethingChanged = true;
					//remove provider from old version of service
					if (serviceTable.get(old) != null){
						serviceTable.get(old).remove(provider);
					}
				}
			}
			//is a update
			if (provider != null && old != null && newS != null && providerTable.get(provider.getAddress()) != null){
				if (provider.isEqual(bd_addr)){
					if (ownServices == null){
						ownServices = new Vector<String>();
					}
					ownServices.remove(old);
					ownServices.add(newS);
					somethingChanged = true;
				}
				if (providerTable.get(provider.getAddress()).contains(old)){
					//first remove then add new only for this provider
					providerTable.get(provider.getAddress()).remove(old);
					providerTable.get(provider.getAddress()).add(newS);
					somethingChanged = true;
					//remove provider from old version of service
					if (serviceTable.get(old) != null){
						serviceTable.get(old).remove(provider);
					}
					//if actual version of service already known, just append this provider
					if (serviceTable.get(newS) != null){
						serviceTable.get(newS).add(provider);
					} else {
						Vector<BluetoothDeviceAddress> tmptmp = new Vector<BluetoothDeviceAddress>();
						tmptmp.add(provider);
						serviceTable.put(newS, tmptmp);
					}
				}
			}
			return somethingChanged;
		}
	
		public void clearServiceTable(){
			this.providerTable.clear();
			this.serviceTable.clear();
			this.addCombination(bd_addr, ownServices);
		}
	
		public Vector<String> getServicesForDevice (BluetoothDeviceAddress bd_addr){
			if (providerTable.get(bd_addr.getAddress()) == null){
				return new Vector<String>();
			}
			return providerTable.get(bd_addr.getAddress());
		}
		public Vector<String> getServicesForDevice (String bd_addr){
			if (providerTable.get(bd_addr) == null){
				return new Vector<String>();
			}
			return providerTable.get(bd_addr);
		}
	
		public Vector<String> getAllAvailableServices (){
			if (serviceTable.keySet() == null || serviceTable.keySet().size() == 0){
				return new Vector<String>();
			} else {
				return new Vector<String> (serviceTable.keySet());
			}
		}
		
		public Vector<BluetoothDeviceAddress> getProviderForService (String serviceName){
			if (serviceTable.get(serviceName) == null){
				return new Vector<BluetoothDeviceAddress>();
			}
			return serviceTable.get(serviceName);
		}
	}
	
	private void processResultAskForService(BluetoothDeviceAddress sender, Vector<String> vector) {
		// TODO Auto-generated method stub
		System.out.println("FloodingPublishDiscovery: ask for Service at " +sender.getAddress() + " processed");
	}
	
	private void processResultFindService(BluetoothDeviceAddress sender, String service) {
		// TODO Auto-generated method stub
		System.out.println("FloodingPublishDiscovery: find Service:" + service + "at: " +sender.getAddress() + " processed");		
	}

	private void processResultGetAllServices(BluetoothDeviceAddress sender, Vector<String> vector) {
		System.out.println("FloodingPublishDiscovery: find all Services answer processed");				
	}

	
	public Vector<String> getXSideServicesForDevice(String device){
		return this.serviceTable.getServicesForDevice(device);
	}
	
	public void getXSideUpdateServicesForDevice (BluetoothDeviceAddress device, Vector<String> services){
		if (this.serviceTable.updateCombination(device, services)){
			this.invokeUpdateForChannelSubscriber(device);
		}
	}

	public void getXSideHostNotLongerReachable (BluetoothDeviceAddress bd_addr){
		if (this.serviceTable.deleteServicesForProvider(bd_addr)){
			invokeSendHostNoLogerReachable(bd_addr);
		}
	}
	
	/** this method realizes the update espacially is for send forward the 
	 * update
	 * 
	 * @param device
	 */
	private void invokeUpdateForChannelSubscriber(BluetoothDeviceAddress device) {
		//prepare for NetworkSend
		String messageString = UPDATE_FOR_SUBSCRIBER;
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(messageString);
		messageContent.add(device);
		messageContent.add(this.serviceTable.getServicesForDevice(device));
		Message msg = new Message (messageContent, true);
		//publish --> target == channel
		this.invokeSendMessage(msg, device);		
	}
}
